Edge Parser
Parser to convert edge templates to invokable functions
Table of contents
This repo is the parser to convert edge templates to a self invoked Javascript function.
Usage
Install the package from npm registry as follows:
npm i edge-parser
yarn add edge-parser
and then use it as follows
import { Parser, EdgeBuffer, Stack } from 'edge-parser'
const filename = 'eval.edge'
const statePropertyName = 'state'
const escapeCallPath = 'escape'
const outputVar = 'out'
const rethrowCallPath = 'reThrow'
const parser = new Parser({}, new Stack(), {
statePropertyName,
escapeCallPath,
})
const buffer = new EdgeBuffer(filename, { outputVar, rethrowCallPath })
parser
.tokenize('Hello {{ username }}', { filename })
.forEach((token) => parser.processToken(token, buffer))
- All the first set of
const
declarations are the config values that impacts the compiled output.
filename
is required to ensure that exceptions stack traces point back to the correct filename.statePropertyName
is the variable name from which the values should be accessed. For example: {{ username }}
will be compiled as state.username
. Leave it to empty, if state is not nested inside an object.escapeCallPath
Reference to the escape
method for escaping interpolation values. For example: {{ username }}
will be compiled as escape(state.username)
. The escape
method should escape only strings and return the other data types as it is.outputVar
is the variable name that holds the output of the compiled template.rethrowCallPath
Reference to the reThrow
method to raise the template exceptions with the current $filename
and $lineNumber
. Check the following compiled output to see how this function is called.
Compiled output
let out = ''
let $lineNumber = 1
let $filename = 'eval.edge'
try {
out += 'Hello '
out += `${escape(state.username)}`
} catch (error) {
reThrow(error, $filename, $lineNumber)
}
return out
You can wrap the compiled output inside a function and invoke it as follows
const fn = new Function('', `return function template (state, escape, reThrow) { ${output} }`)()
const state = { username: 'virk' }
function escape(value: any) {
return value
}
function reThrow(error: Error) {
throw error
}
console.log(fn(state, escape, reThrow))
Parser API
Along with parsing the main template, the parser also exposes the API, that tags can use to selectively parse the content of a tag.
generateAST(jsExpression, lexerLoc, filename)
Parses a string as a Javascript expression. The output is a valid Estree expression
The following example returns a BinaryExpression
const loc = {
start: { line: 1, col: 1 },
end: { line: 1, col: 1 },
}
const filename = 'eval.edge'
parser.utils.generateAST('2 + 2', loc, filename)
transformAst(acornAst, filename)
Transform the acorn AST and make it compatible with Edge runtime. This method mutates the inner nodes of the original AST.
const loc = {
start: { line: 1, col: 1 },
end: { line: 1, col: 1 },
}
const filename = 'eval.edge'
parser.utils.transformAst(parser.utils.generateAST('2 + 2', loc, filename), filename)
tokenize (template, options: { filename })
Returns an array of lexer tokens for the given template. The method is a shortcut to self import the lexer module and then generating tokens.
const tokens = parser.tokenize('Hello {{ username }}', {
filename: 'eval.edge',
})
Output
[
{
"type": "raw",
"line": 1,
"value": "Hello "
},
{
"type": "mustache",
"filename": "eval.edge",
"loc": {
"start": {
"line": 1,
"col": 8
},
"end": {
"line": 1,
"col": 20
}
},
"properties": {
"jsArg": " username "
}
}
]
stringify(expression)
Convert edge or acorn expression back to a string. This is helpful, when you mutate some nodes inside the expression and now want a valid Javascript string out of it.
const expression = parser.utils.generateAST(
'2 + 2',
{
start: { line: 1, col: 1 },
end: { line: 1, col: 1 },
},
'eval.edge'
)
expression.left.value = 3
parser.utils.stringify(expression)
processToken(token, buffer)
You will often find yourself using this method as a tag author, when you want to recursively process all children of your tag
const byPass = {
block: true,
seekable: false,
name: 'bypass',
compile(parser, buffer, token) {
token.children.forEach((child) => parser.processToken(child, buffer))
},
}
and then use it as
@bypass
Hello {{ username }}
@endbypass
Supported Expressions
The following expressions are supported by the parser. Can you also access the list of supported expressions as
import { expressions } from 'edge-parser'
Identifier
The identifier are prefixed with state.
In following statement username
is the identifier
Hello {{ username }}
Literal
A string literal
Hello {{ 'Guest' }}
ArrayExpression
The [1, 2, 3, 4]
is an array expression.
Evens are {{
[1, 2, 3, 4].filter((num) => num % 2 === 0)
}}
ObjectExpression
The { username: 'virk' }
is an Object expression
{{ toJSON({ username: 'virk' }) }}
UnaryExpression
Following are examples of UnaryExpression
.
{{ typeof(username) }}
{{ !!username }}
BinaryExpression
Here {{ 2 + 2 }}
is the binary expression
{{ 2 + 2 }} = 4
LogicalExpression
Following is the example of LogicalExpression
.
{{ username || admin.username }}
MemberExpression
{{ username.toUpperCase() }}
ConditionalExpression
{{ username ? username : 'Guest' }}
CallExpression
{{ upper(username) }}
SequenceExpression
Sequence is not supported in mustache blocks and instead used inside tags. For example:
Everything inside ()
is a sequence expression.
@component('button', text = 'Submit', type = 'Primary')
TemplateLiteral
{{ Hello `${username}` }}
ArrowFunctionExpression
{{
users.map((user) => {
return user.username
})
}}
AwaitExpression
{{ await foo() }}
FunctionDeclaration
{{ function foo () {} }}
BlockStatement
Here the map
callback is the block statement
{{
users.map(() => {})
}}
ChainExpression
Support for optional chaining
{{ user?.username }}
NewExpression
{{ new User() }}
ReturnStatement
In the following example return
keyword is a return statement
users.map((user) => {
return user.username
})
ThisExpression
Support for the this keyword
{{ this.state }}
SpreadElement
Support for the spread element
{{ [...users] }}